"""
ipython的颜色不是由qss决定的，而是由自身决定的。
qss在这个文件中定义。

Created on 2020/8/24
@author: Irony
@email: 892768447@qq.com
@file: console.py
@description: Console Widget
"""
import os
from typing import Tuple, Dict, Callable

from PyQt5.QtCore import QObject, pyqtSignal, QThread, QWaitCondition, QMutex
from PyQt5.QtGui import QTextCursor
from PyQt5.QtWidgets import QMessageBox, QMenu
from qtconsole.manager import QtKernelManager
from qtconsole.rich_jupyter_widget import RichJupyterWidget
from qtconsole import styles
from qtconsole.styles import default_light_syntax_style, default_light_style_sheet

default_dark_style_template = styles.default_template + """\
    .in-prompt { color: #ff00ff; }
    .out-prompt { color: #ff0000; }
"""
default_dark_style_sheet = default_dark_style_template % dict(
    bgcolor='#19232d', fgcolor='white', select="#ccc")
default_dark_syntax_style = 'default'


class ConsoleInitThread(QObject):
    initialized = pyqtSignal(object, object)

    def __init__(self, *args, **kwargs):
        super(ConsoleInitThread, self).__init__(*args, **kwargs)
        self.mutex = QMutex()
        self.wait_condition = QWaitCondition()

    def run(self):
        self.mutex.lock()
        kernel_manager = QtKernelManager(kernel_name='python3')
        kernel_manager.start_kernel()

        kernel_client = kernel_manager.client()
        kernel_client.start_channels()

        # notify to update ui
        self.initialized.emit(kernel_manager, kernel_client)

        # wait for exit
        self.wait_condition.wait(self.mutex)
        self.mutex.unlock()

        # stop channels and kernel
        kernel_client.stop_channels()
        kernel_manager.shutdown_kernel(now=True)  # add now=True; Fix exit error;  200924 liugang

    def stop(self):
        self.wait_condition.wakeAll()


class PMGIpythonConsole(RichJupyterWidget):

    def __init__(self, *args, **kwargs):
        super(PMGIpythonConsole, self).__init__(*args, **kwargs)
        self.is_first_execution = True
        self.confirm_restart = False

        self.commands_pool = []
        self.command_callback_pool: Dict[str, Callable] = {}

    def change_ui_theme(self, style: str):
        """
        改变界面主题颜色
        :param style:
        :return:
        """
        if style == 'Fusion':
            self.style_sheet = default_light_style_sheet
            self.syntax_style = default_light_syntax_style

        elif style == 'Qdarkstyle':
            self.style_sheet = default_dark_style_sheet
            self.syntax_style = default_dark_syntax_style

        elif style.lower() == 'windowsvista':
            self.style_sheet = default_light_style_sheet
            self.syntax_style = default_light_syntax_style

        elif style.lower() == 'windows':
            self.style_sheet = default_light_style_sheet
            self.syntax_style = default_light_syntax_style

    def _handle_kernel_died(self, since_last_heartbit):
        self.is_first_execution = True
        self.restart_kernel(None, True)
        self.initialize_ipython_builtins()
        self.execute_command('')
        return True

    def _handle_execute_input(self, msg):
        super()._handle_execute_result(msg)

    def setup_ui(self):
        self.kernel_manager = None
        self.kernel_client = None
        # initialize by thread
        self.init_thread = QThread(self)
        self.console_object = ConsoleInitThread()
        self.console_object.moveToThread(self.init_thread)
        self.console_object.initialized.connect(self.slot_initialized)
        self.init_thread.finished.connect(self.console_object.deleteLater)
        self.init_thread.finished.connect(self.init_thread.deleteLater)
        self.init_thread.started.connect(self.console_object.run)
        self.init_thread.start()
        cursor: QTextCursor = self._prompt_cursor
        cursor.movePosition(QTextCursor.End)

        _ = lambda s: s
        self.context_menu = QMenu()
        restart_action = self.context_menu.addAction(_('Restart'))
        restart_action.triggered.connect(self._restart_kernel)

    def _custom_context_menu_requested(self, pos):
        self.context_menu.exec_(self.mapToGlobal(pos))

    def _restart_kernel(self, arg1):
        self.is_first_execution = True
        self.restart_kernel(None, True)
        self.initialize_ipython_builtins()
        self.execute_command('')
        return True

    def slot_initialized(self, kernel_manager, kernel_client):
        """
        Args:
            kernel_manager: `qtconsole.manager.QtKernelManager`
            kernel_client: `qtconsole.manager.QtKernelManager.client`

        Returns:
        """
        self.kernel_manager = kernel_manager
        self.kernel_client = kernel_client
        self.initialize_ipython_builtins()

    def initialize_ipython_builtins(self):
        return

    def _update_list(self):
        try:
            super(PMGIpythonConsole, self)._update_list()
        except BaseException:
            import traceback
            traceback.print_exc()

    def _banner_default(self):
        """
        自定义控制台开始的文字
        Returns:
        """
        return 'Welcome To PMGWidgets Ipython Console!\n'

    def closeEvent(self, event):
        if self.init_thread.isRunning():
            self.console_object.stop()
            self.init_thread.quit()
            self.init_thread.wait(500)
        super(PMGIpythonConsole, self).closeEvent(event)

    def execute_file(self, file: str, hidden: bool = False):
        if not os.path.exists(file) or not file.endswith('.py'):
            raise FileNotFoundError(f'{file} not found or invalid')
        base = os.path.basename(file)
        cmd = os.path.splitext(base)[0]
        with open(file, 'r', encoding='utf-8') as f:
            source = f.read()

        self.execute_command(source, hidden=hidden, hint_text=cmd)

    def execute_command(self, source, hidden: bool = False,
                        hint_text: str = '') -> str:
        """

        :param source:
        :param hidden:
        :param hint_text: 运行代码前显示的提示
        :return: str 执行命令的 msgid
        """
        cursor: QTextCursor = self._prompt_cursor
        cursor.movePosition(QTextCursor.End)
        # 运行文件时,显示文件名,无换行符,执行选中内容时,包含换行符
        # 检测换行符,在ipy console中显示执行脚本内容
        hint_row_list = hint_text.split("\n")
        for hint in hint_row_list:
            if hint != "":
                cursor.insertText('%s\n' % hint)
                self._insert_continuation_prompt(cursor)
        else:
            # 删除多余的continuation_prompt
            self.undo()

        self._finalize_input_request()  # display input string buffer in console.
        cursor.movePosition(QTextCursor.End)
        if self.kernel_client is None:
            self.commands_pool.append((source, hidden, hint_text))
            return ''
        else:
            return self.pmexecute(source, hidden)

    def _handle_stream(self, msg):
        print(msg['content'].get('text'), msg)
        parent_header = msg.get('parent_header')
        print('msg_id',parent_header['msg_id'])
        if parent_header is not None:
            msg_id = parent_header.get('msg_id')  # 'fee0bee5-074c00d093b1455be6d166b1_10'']
            if msg_id in self.command_callback_pool.keys():
                callback = self.command_callback_pool.pop(msg_id)
                assert callable(callback)
                callback()
        cursor: QTextCursor = self._prompt_cursor
        cursor.movePosition(QTextCursor.End)
        super()._handle_stream(msg)

    def append_stream(self, text):
        """重写的方法。原本before_prompt属性是False。"""
        self._append_plain_text(text, before_prompt=False)

    def pmexecute(self, source: str, hidden: bool = False) -> str:
        """
        执行代码并且返回Msgid
        :param source:
        :param hidden:
        :return:
        """
        is_legal, msg = self.is_source_code_legal(source)
        if not is_legal:
            QMessageBox.warning(self, '警告', msg)
            source = ''
        msg_id = self.kernel_client.execute(source, hidden)
        self._request_info['execute'][msg_id] = self._ExecutionRequest(msg_id, 'user')
        self._hidden = hidden
        if not hidden:
            self.executing.emit(source)
        return msg_id
        # super()._execute(source, hidden)

    def is_source_code_legal(self, source_code: str) -> Tuple[bool, str]:
        """
        判断注入到shell的命令是否合法，不合法的话，就避免执行这个函数。
        :param source_code:
        :return:
        """
        return True, ''


if __name__ == '__main__':
    import sys
    import cgitb
    from PyQt5.QtWidgets import QApplication

    cgitb.enable(format='text')

    app = QApplication(sys.argv)
    w = PMGIpythonConsole()
    w.show()
    w.setup_ui()
    sys.exit(app.exec_())
